从双亲委派模型到 jdbc

引子

双亲委派模型,jdbc,貌似没啥关系,之间的联系还得从周志华老师的《深入理解java虚拟机》这本书说起。

image-20200319115741286

上图可以看到,jdbc 这种涉及 SPI 的类加载方式破坏了双亲委派模型,接下来,我们来具体分析一波。

回顾 — 双亲委派模型

类加载器

谈到双亲委派模型,必然是要谈到类加载器啦,而类加载器要做的事情,就是完成类加载中的第一个动作,顺带回顾一下类加载的三个动作:

  1. 通过类的全限定名「一般是 .class 文件的形式,也可以是其他的,没有限定」产生一个二进制数据流;
  2. 将该二进制数据流解析为方法区的运行时数据结构;
  3. 在 Java 堆中创建一个表示该类型的java.lang.Class类的实例,作为方法区这个类的各种数据的访问入口。

讲完类加载器的工作,再来回顾一下类加载器的分类,类加载器的分类可以从两个角度看,

  1. 如果是 jvm 角度看,那就只需要分为 启动类加载器 「native 语言写的」和 其他类加载器「java 写的」;
  2. 如果从程序员角度看,那就分为四类,一类是 启动类加载器「Bootstrap ClassLoader」,一类是 扩展类加载器「Extension ClassLoader」,一类是 应用程序类加载器「Application ClassLoader,这个就是我们开发程序的默认使用过的类加载器」,最后一类是 自定义类加载器「自己实现的类加载器,其实只要去继承CLassLoader 下的 findCLass() 方法就行了,这样是符合双亲委派模型的」。

双亲委派模型

接下来我们从程序员角度的分类来看双亲委派模型。

概念 「什么是双亲委派模型」

除了顶层的启动类加载器以外,所有的类加载器都应该有父类加载器,这里的父子关系不是通过继承来的,而是通过组合关系「要特别注意哦,是组合关系,是 has-a 关系,这个地方跟后面为何 jdbc 要违反双亲委派模型有不可分割的关系哈」

模型怎么用「双亲委派模型的工作流程」

如果一个类加载器收到了类加载的请求,先把这个请求委派给父类加载器去完成(所以所有的加载请求最终都应该传送到顶层的启动类加载器中),只有当父加载器反馈自己无法完成加载请求(它的搜索范围没有找到所需要的类)时,子加载器才会尝试自己去加载。

具体我们在源代码中可以看到,这样看的更清楚一点,在 ClassLoader 类中,完成这个工作流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先检查是否已经加载过了,加载过了就没必要加载了。
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 可以看到这里是个递归调用,就是意味着将该请求不断地向上委派
c = parent.loadClass(name, false);
} else {
// 没有老父亲了,判断是不是 BootStrap ClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats

sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

为何要引入这个双亲委派模型,直接用一个类加载器不好吗?

当然不好!我觉得双亲委派模型是很有必要的:能够最大限度的保证内库的安全性,比如 String 类,如果我自己去实现,双亲委派模型是不会加载我这个类的,而是会使用启动了加载器去加载内库中的 String 类。

换句话说,双亲委派带来的这个好处,就是 java类随着它的类加载器一起具备了一种带有优先级的层次关系,内库的类优先级高,所以就更加安全了

那我弄多个类加载器,我不搞这种工作流程可不可以?

不这样的话,很容易导致一个类被多个不同的类加载器加载,这样会产生很多本应该一样的二进制流,但是由于类加载器不同,在方法区就不同了。

既然这么好,为何要违反这个模型,违反不会带来问题吗?

好的确是好啊,但是这个模型他有致命的弱点啊,就是 has-a,也就是他们只是组合的关系,这样会导致一个问题,就是我顶层加载器加载类时,遇到问题了,比如说 jdbc,就是顶层加载器加载了 driver 这个接口,但是没有实现类,这个时候顶层加载器已经在加载这个类了,但是卡住了,此时双亲委派模型也帮不上忙了啊,因为这个模型并不允许你顶层加载器去获得下一级的加载器去帮忙加载类,所以此时就必须违背这个模型去做一些事情了。

回顾 — jdbc

在正式讲 jdbc 破坏双亲委派模型之前,我们先来好好回顾一下 jdbc 的概念,这是 mybatis 的基础。

定义

JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用 Java 语言编写的类和接口组成。JDBC提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。

一句话总结就是,jdbc 是 sun 公司提出来 java 操作 数据库的规范,具体如何去实现,需要各大厂商自己实现。

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package 类加载器;



import java.sql.*;
import java.util.ArrayList;
import java.util.List;

// 这里使用的是 Class.forName(""),没有使用 spi 去加载,所以这个并没有违反双亲委派模型
/**
* 在mysql中创建test数据库,并建立user表,两个字段,name和age
*/
public class jdbc {
/**
* 数据库相关参数
*/
// maven 导包就好,mysql 8.0之后的包,官方也是建议使用 SPI 哦!!
// 使用 spi 就可以不用硬编码了,不用每次换包都改代码了!
public static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";

//连接数据库的url,各个数据库厂商不一样,此处为mysql的;后面是创建的数据库名称
public static final String JDBC_URL = "jdbc:mysql://localhost:3306/test";

//连接数据库所需账户名
public static final String JDBC_USERNAME = "root";

//用户名对应的密码,我的mysql密码是123456
public static final String JDBC_PASSWORD ="jwyjwy9951206-=-";


public static void main(String[] args) {
List<Student> students = new ArrayList<Student>();

Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;

try {
//第一步:加载Driver类,注册数据库驱动
Class.forName(JDBC_DRIVER);
//第二步:通过DriverManager,使用url,用户名和密码建立连接(Connection)
connection = DriverManager.getConnection(JDBC_URL, JDBC_USERNAME, JDBC_PASSWORD);
//第三步:通过Connection,使用sql语句打开Statement对象;
preparedStatement = connection.prepareStatement("select * from user where age =?");
//传入参数,之所以这样是为了防止sql注入
preparedStatement.setInt(1, 21);
//第四步:执行语句,将结果返回resultSet
resultSet = preparedStatement.executeQuery();
//第五步:对结果进行处理
while (resultSet.next()){
String name = resultSet.getString("name");
int age = resultSet.getInt("age");

Student student = new Student();
student.setAge(age);
student.setName(name);
students.add(student);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}finally {
//第六步:倒叙释放资源resultSet-》preparedStatement-》connection
try {
if (resultSet!=null && !resultSet.isClosed()){
resultSet.close();
}
} catch (SQLException e) {
e.printStackTrace();
}

try {
if(preparedStatement!=null &&
!preparedStatement.isClosed()){
preparedStatement.close();
}
} catch (SQLException e) {
e.printStackTrace();
}

try {
if(connection!=null && connection.isClosed()){
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}

for (Student student:students) {
System.out.println(student.getName()+"="+student.getAge());
}

}
}

class Student{
private String name;
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

总结一下,有以下几步:

  1. 使用 DriverManager 去建立连接,这里是直接用 Class.forName()去进行 driver驱动的加载,这样的话这个driver 是由 Application ClassLoader 加载的,而 DriverManager 是由启动类加载器加载的,由于 driver 先加载了,所以这里并不会违反双亲委派原则,在下面我还会详细讲到这个加载过程;
  2. 建立连接后,通过 connection 对象,传入 sql 语句,包装成 Statement 对象,然后可以对其传参;
  3. 然后执行 sql 语句,会返回结果结果集 ResultSet;
  4. 处理结果即可;
  5. 最后释放资源。

jdbc & 双亲委派模型的爱恨情仇

主要参考:

https://blog.csdn.net/justloveyou_/article/details/72231425

在讲正题之前,还是要再铺垫一下,先介绍几个需要知道的概念:SPI 机制、线程上下文类加载器。

SPI

简介

SPI(Service Provider Interface),针对厂商或者插件的一种机制。我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

————————————————
版权声明:本文为CSDN博主「sigangjun」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sigangjun/article/details/79071850

约定

当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件,该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能找到服务接口的实现类,不需要在代码里指定。jdk提供服务实现查找的工具类:java.util.ServiceLoader

带来的问题

SPI (服务提供接口)是在 rt.jar 下定义的,这是系统核心库,是由启动类加载器去加载的,但是接口的实现却在各供应商提供的 jar 包下,启动类加载器肯定是无法去进行加载的,所以此时双亲委派模型无法解决这个问题。

线程上下文类加载器

正是因为 SPI 带来的问题,所以我们引入 线程上下文类加载器「Context ClassLoader」 这个武器来进行双亲委派模型的违反。

线程上下文类加载器是从 JDK 1.2 开始引入的。Java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器「Application ClassLoader」,在线程中运行的代码可以通过此类加载器来加载类和资源。

有了上下文类加载器,那就可以直接在 SPI 中去加载各厂商提供的实现类了。

jdbc 的加载

jdbc 可以通过两种方式进行加载:

  1. 这种方式并没有违反双亲委派模型,因为其并没有调用 SPI 接口,直接去硬编码加载 Driver 驱动,所以根本不会用到 BootStrap ClassLoader 去调用 SPI。
1
2
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection(JDBC_URL, JDBC_USERNAME, JDBC_PASSWORD);
  1. SPI 机制去加载 Driver 驱动,可以避免硬编码,驱动改了也无需修改代码。因为 jdk 默认就是 spi 机制,所以其实上面那一行是多余的,去掉 Class.forName("com.mysql.cj.jdbc.Driver");程序是照样执行的。

    我们可以深入源码去看一下实现的具体步骤:

    • 在 DriverManager 的 static 代码块中
    1
    2
    3
    4
    5
    6
    7
    8
    /**
    * Load the initial JDBC drivers by checking the System property
    * jdbc.properties and then use the {@code ServiceLoader} mechanism
    */
    static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
    }
    • 我们注意到有个 loadIntialDrivers(),继续看
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    private static void loadInitialDrivers() {
    String drivers;
    try {
    drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
    public String run() {
    return System.getProperty("jdbc.drivers");
    }
    });
    } catch (Exception ex) {
    drivers = null;
    }
    // If the driver is packaged as a Service Provider, load it.
    // Get all the drivers through the classloader
    // exposed as a java.sql.Driver.class service.
    // ServiceLoader.load() replaces the sun.misc.Providers()

    AccessController.doPrivileged(new PrivilegedAction<Void>() {
    public Void run() {
    // 这里是重点!!!!!!!
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();

    /* Load these drivers, so that they can be instantiated.
    * It may be the case that the driver class may not be there
    * i.e. there may be a packaged driver with the service class
    * as implementation of java.sql.Driver but the actual class
    * may be missing. In that case a java.util.ServiceConfigurationError
    * will be thrown at runtime by the VM trying to locate
    * and load the service.
    *
    * Adding a try catch block to catch those runtime errors
    * if driver not available in classpath but it's
    * packaged as service and that service is there in classpath.
    */
    try{
    while(driversIterator.hasNext()) {
    driversIterator.next();
    }
    } catch(Throwable t) {
    // Do nothing
    }
    return null;
    }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    if (drivers == null || drivers.equals("")) {
    return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
    try {
    println("DriverManager.Initialize: loading " + aDriver);
    Class.forName(aDriver, true,
    ClassLoader.getSystemClassLoader());
    } catch (Exception ex) {
    println("DriverManager.Initialize: load failed: " + ex);
    }
    }
    }
    • 我们看到了我上文提及的 ServiceLoader,我们说过他其实就是去进行服务实现查找的,继续看 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);做了些什么「其实我们肯定都能猜到了,肯定是拿到了驱动的列表。」
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
    }

    public static <S> ServiceLoader<S> load(Class<S> service,
    ClassLoader loader)
    {
    return new ServiceLoader<>(service, loader);
    }
    • 在这里,终于出现了线程上下文类加载器,然后再经过一系列的函数调用「我就不展开了,还是非常复杂的,我们来看看最终的有效代码」。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    private boolean hasNextService() {
    if (nextName != null) {
    return true;
    }
    if (configs == null) {
    try {
    // 等同于 Class.forName()
    String fullName = PREFIX + service.getName();
    if (loader == null)
    configs = ClassLoader.getSystemResources(fullName);
    else
    configs = loader.getResources(fullName);
    } catch (IOException x) {
    fail(service, "Error locating configuration files", x);
    }
    }
    while ((pending == null) || !pending.hasNext()) {
    if (!configs.hasMoreElements()) {
    return false;
    }
    pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
    }

所以,就是利用线程上下文类加载器去完成违反双亲委派模型的行为的。

拓展

除了 SPI 机制,违反了双亲委派模型,在 Spring 中 和 Tomcat 中也同样违反了双亲委派模型。

「Tomcat这一块没太搞明白。。。。。」

  • Tomcat 中违反的原因是每个 web应用程序对应的类库都应该是互相隔离的,但是如果是双亲委派机制,那么都会去用顶层加载器加载相应的类,那么一个相同的类会被加载多次,而又是属于不同的webapp,导致发生冲突,所以在 Tomcat 中是 WebApp 自己直接加载,不转发给上级加载器,也就是所谓的“子优先”。「这块我自己也有些不确定,在周志华老师的书中也讲到了这个例子,但是我也没太明白他讲这个例子的意思。。。」

    有个跟我类似想法的博客,写的比较详细,可以看看:https://www.cnblogs.com/aspirant/p/8991830.html

    这个谈到了子优先,不过也没太看明白:https://zhuanlan.zhihu.com/p/24168200

  • Spring,这个问题周志华老师的书中也有提及。

    如下:如果有 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?

    spring根本不会去管自己被放在哪里,它统统使用线程上下文类加载器来加载类,而线程类加载器默认设置为了WebAppClassLoader。也就是说,哪个WebApp应用调用了Spring,Spring就去取该应用自己的WebAppClassLoader来加载bean。

Thank you for your accept. mua!
-------------本文结束感谢您的阅读-------------